Skip to Content
This documentation is provided with the HEAT environment and describes this HEAT instance.

Interface control document

1. Identification

ItemValue
DocumentICD-CMFOLDER-001 rev 2
ProductFolder upload module (FolderUpload in the module registry, package HEAT.FolderUploadModule)
Product versions covered5debe79
PeerA directory on a filesystem reachable by the capture workstation
HostHEAT Capture, HEAT.Capture.ModuleSDK 2.0.5, net10.0
Settings sectionHEAT.FolderUploadModule
Data modelHEAT.Proto.Data.FolderUploadData
Date2026-09-02
OwnerVRAI Simulation

2. Scope

This document defines the three interfaces the module actually has: the change notification it receives from the operating system for the watched directory, the file read it performs on the paths that notification gave it, and the packet it publishes to HEAT Capture.

There is no network listener, no vendor SDK, no handshake and no acknowledgement. The module never writes to the watched folder and never modifies, moves or deletes a file it has read.

The module is declared ModuleType.Data with StartupType = ModuleStartupType.BeforeSession. The stop type is left at the SDK default, ModuleStopType.BeforeSessionEnd, which is what makes the module’s publications land inside the session: the host stops BeforeSessionEnd modules before it ends the session.

3. Interface inventory

IDNameDirectionTransportPeer
IF-CMFOLDER-01Directory change notificationFilesystem to module.NET FileSystemWatcher over the operating system’s directory change notificationWatched directory
IF-CMFOLDER-02File content readFilesystem to moduleFile.ReadAllBytesAsync on the notified pathWatched directory
IF-CMFOLDER-03FolderUploadData publicationModule to hostModule SDK publication pathHEAT Capture

4. IF-CMFOLDER-01 Directory change notification

4.1 Transport and addressing

The address is the Folder setting, default C:\FolderToUpload. It is passed straight to the FileSystemWatcher constructor with no normalisation, no existence check at that point and no restriction to a local volume. A UNC path or a mapped drive is accepted; whether notification works over it is a property of the remote filesystem, not of this module.

If the directory does not exist when the module initialises, the module logs Folder Upload folder does not exist: {Folder}. Creating it. and creates it. Creation happens inside InitialiseModule, so it precedes any session.

4.2 Watcher configuration

PropertyValue in this revisionConsequence
PathThe Folder settingSee 4.1
NotifyFilterNotifyFilters.FileName | NotifyFilters.CreationTimeFileName is the filter that makes Created fire. CreationTime affects only Changed, which has no handler, so it has no observable effect here
FilterNot set, left at the .NET defaultEvery name matches. There is no extension or name-pattern filtering
IncludeSubdirectoriesNot set, so falseOnly the top level of the watched folder is observed. A file created in a subdirectory is never notified
InternalBufferSizeNot set, so the .NET default of 8192 bytesSee the overflow row in section 8
EnableRaisingEventstrue, set in the object initialiserSee 4.4
Handlers attachedCreated onlyChanged, Deleted, Renamed and Error are not subscribed

The property defaults the module does not set are defined by the .NET FileSystemWatcher documentation, not by this module.

4.3 Connection lifecycle and observation window

There is no connection to establish or re-establish. The watcher is created once per capture start and destroyed once per capture stop; there is no reconnect logic and none is meaningful.

StartCapture clears the accumulated path list, then constructs the watcher. If construction throws (the folder was removed after initialisation, a network path stopped resolving) the module logs Error setting up file watcher and returns false, so the capture loop never starts. On success it logs File watcher started for folder: {Folder}.

EndCapture does not stop the watcher first. It awaits DelayBeforeUpload, default 30 seconds, and only then detaches the handler, disables events and disposes the watcher. The observation window is therefore module start through the end of that delay, which extends past session end:

[module start]───watching───[session stop requested]───still watching for DelayBeforeUpload───[watcher torn down, files read]

A file created before the module starts, or after the delay elapses, produces no notification and is never read. CleanupAsync calls the same teardown a second time; it is a no-op because teardown nulls the field.

4.4 Startup gap, FOLDER-02

EnableRaisingEvents = true is set inside the object initialiser, and the Created handler is attached on the following statement. Notifications raised between those two points have no subscriber and are discarded with no log entry. A file created in that window is silently absent from the session. This is audit finding FOLDER-02 and it is a property of this revision, not of the operating system’s notification mechanism. The window is short but it is not empty, and it cannot be closed from configuration.

4.5 Authentication

None, on either direction. The module presents no credential to the filesystem beyond the HEAT Capture process token, and it applies no test to the origin of a file. Any process that can create a file in the watched directory has its bytes published into the session.

4.6 File eligibility

There are no eligibility rules. Every Created notification appends its FullPath to an in-memory list under a lock, and every entry in that list is read at teardown. Specifically:

  • No filtering by extension, name, size, age or content.
  • The module never enumerates the directory. A file already present when the module starts is not read, because eligibility comes from a notification and not from a directory scan.
  • The list is not de-duplicated. A name created, deleted and created again inside the window appears twice and is published twice.
  • Directories are not excluded. A subdirectory created directly in the watched folder is notified as Created and is appended to the list like a file. Its read fails; see section 8.
  • A writer that stages content as one name and renames it to its final name inside the watched folder is only notified for the staging name. The rename raises Renamed, which has no handler. The staging path no longer exists at read time, so the read fails and the final file is never published. Writers must create the file at its final path, or stage it outside the watched folder and move it in.

4.7 What happens to a detected file

Nothing, on the filesystem. The module reads it and leaves it exactly where it is, with its name, timestamps and content untouched. There is no move to a processed folder, no deletion, no marker file and no lock. Because the path list is cleared at teardown and rebuilt from notifications on the next start, a file left behind is not re-published by a later session, but nothing on disk records that it was already read.

5. IF-CMFOLDER-02 File content read

Reads happen only during teardown, after the DelayBeforeUpload wait, sequentially, in notification order, one File.ReadAllBytesAsync per recorded path with the host’s stop cancellation token. The module logs Processing file: {FilePath} before each read and File read successfully. Size: {Size} bytes after it.

The read is a single whole-file load. There is no streaming, no chunking, no size cap and no configurable maximum. The bytes are then copied a second time into a protobuf ByteString, so peak working set is on the order of twice the file size for the file currently being processed.

5.1 A file still being written, or locked

The module has no completeness test. It does not check for a sentinel file, a size that has stopped changing, an exclusive-open probe or a retry. DelayBeforeUpload is the only mechanism the module offers for letting a writer finish, and it is a fixed wait, not a readiness check.

Two outcomes follow, and a consumer cannot distinguish them from the payload:

  • The writer has closed the file but the module read it while it was still incomplete only if the writer left it readable mid-write. In that case the read succeeds and a truncated payload is published as though it were the whole file. Nothing in the payload or the log flags it.
  • The writer still holds an exclusive lock. The read throws an IOException, the module logs Error reading file: {FilePath}, and the file is skipped for that session with no retry and no later attempt.

6. IF-CMFOLDER-03 FolderUploadData publication and data model reference

6.1 Wire format

HEAT.Proto.Data.FolderUploadData has exactly one field:

FieldNumberTypeContent
content1bytesThe complete file, byte for byte, with no transformation

No encoding is assumed or declared, no compression is applied, and no text conversion happens. See HEAT ProtoLib’s message reference for the message itself.

6.2 Published data and naming

There are no component or reading names to document, because this module publishes no SimulationFrame, no entity and no component. It publishes a root-level opaque payload. Consumers identify it by the packet’s Any type URL, type.googleapis.com/HEAT.Proto.Data.FolderUploadData.

ItemValue
Packets per fileExactly one. There is no batching and no manifest
EnvelopeDataPacket with TimestampUnixMs set to UTC Unix milliseconds at publish time and Payload set to Any.Pack(FolderUploadData)
Node attributionNodeId when OverrideNodeId is true, otherwise -1
OriginUuidLeft empty by the module. The host fills it from the authenticated user when it is empty

6.3 What is not published

The file name, extension, full path, size, creation and modification times, and any hash or ordering marker are all absent from the payload. The only per-file identifier that exists anywhere is the Processing file: {FilePath} log line, which is not part of the captured data.

A consumer therefore cannot tell which file a payload came from, cannot tell two payloads apart when their content is identical, and cannot reconstruct the folder layout. If provenance matters downstream it has to be encoded inside the file’s own content by whatever writes it.

The packet timestamp is not a proxy for when the file appeared. Every file recorded in a window is published in one burst at teardown, so all packets from a session carry near-identical timestamps regardless of how far apart the files were created.

6.4 Cadence

HandleCapture unconditionally returns null, so the SDK capture loop publishes nothing on any cycle. The loop still runs at Interval, default 1000 ms, doing no work. All publication for a session happens in one burst inside EndCapture.

Interval therefore has no effect on this module’s output. Interval is marked [HideSetting] in the settings type and is not exposed in the settings UI.

7. Stop timing budget

This module is unusual among capture modules in that all of its work happens during shutdown, so the host’s stop budget is part of its interface rather than an operational detail.

The module overrides the inherited Timeout setting to a computed value, max(DelayBeforeUpload.TotalSeconds × 1.1, 5) seconds, with a no-op setter and a [HideSetting] [ReadOnly] declaration. The intent is that the host’s stop budget outlasts the wait inside EndCapture with roughly ten percent of headroom for the reads. At the default 30 second delay, Timeout is 33 seconds, leaving about 3 seconds for reading and publishing every file recorded during the session. A large file set, a slow volume or a network path can exhaust that headroom.

The host applies the value in ModuleManager.SafeStopTask: it starts from a fixed 30 second budget and substitutes the module’s Timeout only when settings.Timeout.Seconds is 5 or greater. TimeSpan.Seconds is the seconds component of the interval, not its total, so the guard misreads any computed Timeout whose total exceeds a minute while its seconds component is 0 to 4. When that happens the host silently uses 30 seconds, which is shorter than the delay the module is about to wait out, the wait is cancelled, and no file from that session is published.

DelayBeforeUploadComputed TimeoutSeconds componentBudget the host usesOutcome
30 s (default)33 s3333 sDelay completes, about 3 s left for all reads
54 s59.4 s5959.4 sDelay completes
55 s to 59 s60.5 s to 64.9 s0 to 430 sDelay is cancelled. Nothing is published
1 min1 min 6 s61 min 6 sDelay completes
1 min 50 s to 1 min 53 s2 min 0.3 s to 2 min 4.3 s0 to 430 sDelay is cancelled. Nothing is published

The fault recurs wherever the computed Timeout lands in the first five seconds of a minute. It is not in the repository audit as a finding. Until it is fixed, choose DelayBeforeUpload so that the value multiplied by 1.1 does not fall in the first five seconds of a minute, and verify a real session end rather than assuming the delay was honoured.

8. Error behaviour

ConditionWhereObservableEffect on the interface
Watched folder absent at initialisationInitialiseModuleWarning Folder Upload folder does not exist: {Folder}. Creating it.Folder created, module continues. Nothing at a previous location is picked up
Folder cannot be createdInitialiseModuleSDK logs {Module} InitialiseModule failed.Initialisation returns false. Module does not start
Watcher construction failsStartCaptureError Error setting up file watcherStartCapture returns false. No file is captured for that session
File created between enabling events and attaching the handlerStartCaptureNo logNotification discarded. FOLDER-02
Notification burst exceeds the watcher’s internal bufferWatcherNo log. No Error handler is attached, so the overflow is never surfacedAffected notifications lost silently
File created in a subdirectoryWatcherNo logNot notified. IncludeSubdirectories is false
Recorded path no longer exists at read timeProcessFileError Error reading file: {FilePath}File skipped. No retry. Covers a deleted file and a rename staging path
Recorded path locked by its writerProcessFileError Error reading file: {FilePath}File skipped. No retry
Recorded path exceeds the .NET whole-file read limit of int.MaxValue bytesProcessFileError Error reading file: {FilePath}File skipped. The module imposes no size limit of its own
Recorded path is a directoryProcessFileError Error publishing Folder Upload data from file: {FilePath}Skipped. File.ReadAllBytesAsync on a directory throws UnauthorizedAccessException, which is not an IOException, so it is reported by the second handler
Stop budget exhausted during the delayEndCaptureHost logs Stop timed out for module {ModuleName}, SDK logs Error ending capture in module {Module}No file published. See section 7
Stop budget exhausted partway through the readsProcessFileError Error publishing Folder Upload data from file: {FilePath} once per remaining fileRemaining files dropped. Cancellation is not an IOException, so it lands in the second handler
Capture paused when the module stopsHostModule logs Folder Upload data published successfully. The host discards the packet with no logNothing is stored. The module’s success log is not evidence that a payload was recorded
Watcher teardown throwsCleanupFileWatcherWarning Error cleaning up file watcherSwallowed. Teardown continues

No condition in this table causes a retry, and no condition is reported back to the writing process. A failed file is indistinguishable, from the captured data alone, from a file that was never created.

9. Units and conventions

ConceptConvention
contentRaw file bytes. No character encoding, framing or content type is declared or inferred
Packet time baseHost wall clock, UTC Unix milliseconds, taken at publish time during teardown. Not the file’s creation or modification time
DelayBeforeUpload and Timeout.NET TimeSpan. In JSON configuration, the "hh:mm:ss" string form, for example "00:01:00"
PathsFileSystemEventArgs.FullPath as the watcher reports it, absolute, used verbatim for the read and never published
Node attribution-1 means no override, so the host applies its own routing

10. Configuration affecting the interface

SettingTypeDefaultInterface effect
FolderstringC:\FolderToUploadThe watched address. Created at initialisation if absent
DelayBeforeUploadTimeSpan30 secondsExtends the observation window past session stop, delays every read and publication by that amount, and determines the computed Timeout. See section 7
EnabledbooleantrueWhether the module runs at all
OverrideNodeIdbooleanfalseWhether NodeId is applied instead of -1
NodeIdinteger0Node the packets are attributed to when OverrideNodeId is true
Intervalinteger, [HideSetting]1000 msNone. HandleCapture always returns null
TimeoutTimeSpan, computed, [HideSetting] [ReadOnly]max(DelayBeforeUpload × 1.1, 5) secondsThe host’s stop budget, subject to section 7. Cannot be set: the setter is a no-op

The watcher’s filter, subdirectory recursion, buffer size and event subscriptions are not settings. There is no way from configuration to restrict which files are picked up, to observe subdirectories, to enlarge the notification buffer, or to change the retry behaviour, because there is none.

FolderUploadSettings implements IDataModuleSettings directly rather than deriving from BaseDataModuleSettings, which is why it redeclares the generic members. Their meanings are unchanged; see Manage capture modules and the Module SDK ICD.

11. Compatibility and versioning

There is no version negotiation on any of the three interfaces. The filesystem side has no protocol to version, and FolderUploadData carries a single bytes field at number 1, so it is stable under protobuf’s own compatibility rules.

The module targets net10.0 and references HEAT.Capture.ModuleSDK 2.0.5. It declares no [ModuleCapability], so HEAT Capture applies no capability mediation to its filesystem access.

Identifying which artifact a deployment is running requires care. The release workflow names the package ZIP from the full SemVer on any non-main branch, but derives both the GitHub release tag and the download URL written into heat.module.json from major.minor.patch. A prerelease build and a stable build therefore both publish into the tag vX.Y.Z, with different ZIP filenames, and the last run’s manifest wins. This is audit finding FOLDER-01. Identify the deployed artifact by the ZIP filename and checksum recorded in the installed manifest’s source block, not by the release tag.

12. Security considerations

The watched directory’s access control list is the entire security boundary. There is no authentication, no signature check and no content inspection, so any process that can create a file in that directory can put arbitrary bytes into a capture session, at arbitrary size, and the payload carries nothing that identifies the writer.

The default Folder of C:\FolderToUpload sits at the root of the system drive and inherits that root’s access control rather than an access control chosen for this purpose. Treat the default as a placeholder: point Folder at a directory created for the deployment, with an explicit access control list granting write access only to the process that produces the files and read access only to HEAT Capture.

Because file content is published verbatim, whatever the dropped files contain enters the session record unfiltered, including anything personal or secret that a log or export happens to carry. Screen what is written into the watched folder at the source; this module will not.

13. Conformance

  1. Point Folder at a writable directory. Start a capture session and confirm the log records File watcher started for folder: {Folder}.
  2. Create one file in that directory after the watcher line appears. Confirm File created: {FullPath}, then stop the session and confirm Found 1 file(s) to process, File read successfully. Size: {Size} bytes and Folder Upload data published successfully.
  3. Confirm the captured session contains one packet whose payload type URL is type.googleapis.com/HEAT.Proto.Data.FolderUploadData and whose content byte length equals the file’s size on disk.
  4. Confirm the source file is still present and unmodified in the watched folder, and that starting a second session without creating anything publishes no packet.
  5. Place a file in the directory before starting a session and confirm it is never published. This verifies that eligibility comes from notification and not from a directory scan.
  6. Create a file in a subdirectory of the watched folder during a session and confirm it is never published.
  7. Confirm the timing of section 4.3: create a file after the session stop is requested but before DelayBeforeUpload elapses, and confirm it is captured.
  8. Hold a file open with an exclusive lock across the whole window and confirm Error reading file: {FilePath} and that no further attempt is made.
  9. Exercise section 7 before committing to a value. Set DelayBeforeUpload to 55 seconds, stop a session, and confirm whether Stop timed out for module {ModuleName} and Error ending capture in module {Module} appear and no packet is produced. Then confirm the value the deployment will actually use does not exhibit this.
  10. Exercise FOLDER-02 by creating a file at the instant the module starts, repeatedly, and confirm whether it is captured. A file created in that window is expected to be lost until FOLDER-02 is fixed.

14. Revision history

RevDateProduct versionChange
12026-09-025debe79Initial issue.
22026-09-025debe79Rewritten from source. Full watcher configuration, eligibility, read and publication contract, stop timing budget, and error table.
Opens your browser's print dialog. Choose "Save as PDF" as the destination.