Skip to content

LIST-FILES on the wire, and two corrections to the QFORM framing

Date: 2026-07-29 Capture: E:\Dev\Ronny\X25Emulator\pcap\fa-access-secret-102-to-100-2026-07-29.pcapng Direction: node 102 (TCP 45164) to node 100 (TCP 10362), HDLC LAPB over TCP Status: two framing facts corrected and VERIFIED; the directory-entry record is PARTIALLY decoded


1. Summary

This capture already contained a successful remote directory listing. Decoding it produced two corrections to how we frame a QFORM body, both confirmed by measurement, and a first look at the record a file server returns per directory entry.

It also showed a second service name on the wire, *FA-USER, which had not been recorded before.


2. CORRECTION: the body starts 38 bytes into the LAPB frame, not 34

Every earlier walk of this capture used an offset of 34. That is wrong by 4 bytes.

Worked example, frame 27 (server to client), bytes after the opening 0x7E flag:

off 0  09              LAPB address
off 1  AC              LAPB control
off 2  21 13           SINTRAN protocol id
off 4  00 0E           frame type - 0x0E marks a data frame
off 6  00 66 00 64     ... envelope fields ...
...
off 30 07 F0 00 02 81 00 90 81
off 38 92 00 0C        <- FIRST QFORM TAG

At offset 38 the body opens 92 000C (a class-1 two-byte integer) and every subsequent field closes cleanly on the F2 00FF end-of-list selector. At offset 34 the walk starts mid-field and desynchronises.

The same offset holds for client-to-server frames (frame 23 also opens 92 000C at 38).

This supersedes the 80 00 00 01 "unresolved body opener" noted in XMSG-FA-ACCESS-PASSWORD-ON-THE-WIRE-2026-07-29.md section 6.3. That opener was an artefact of starting four bytes early. The bytes at offsets 30-37 are header, not body.


3. CORRECTION: a length byte of 0x80 is an escape MARKER

QformReader previously threw on a length byte of 0x80, on the grounds that the accumulation arithmetic at ram:0x7d48 was unresolved and no captured frame exercised it. Both halves of that statement are now obsolete: this capture exercises it, and the rule is simply that the real length is the byte that follows.

Frame 23 carries a constructed value tagged 8C 80 46. Read as an escape marker, the declared length is 0x46 = 70. The contents account for exactly 70 bytes:

Field Bytes
B0 3E + string 2 + 62 = 64
A2 0000 1 + 2 = 3
A2 FFFF 1 + 2 = 3
total 70

This is an arithmetic confirmation, not merely a clean parse: the three inner fields close exactly on the declared boundary. Any other reading of 0x80 would leave the constructed value over- or under-run.

Measured effect

Same capture, same 34 qualifying data frames, one variable changed at a time:

Configuration Clean walks
offset 34, 0x80 refused (the old code) 19 / 34
offset 38, 0x80 refused 28 / 34
offset 38, 0x80 as escape marker 32 / 34

Both changes are now in E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol\Qform\QformReader.cs, with tests in E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol.Tests\QformReaderTests.cs.

Two frames still fail and the cause is not established. Do not tune anything further against them until it is.


4. The directory listing itself

4.1 The request (frame 23, client to server)

92 000C                       (meaning not established)
92 0002                       (meaning not established)
F2 0001  92 0078              field 1 = 120
F2 0002  8C 80 46             field 2, constructed, 70 bytes:
           B0 3E  "(SECRET)'..." padded to 62
           A2 0000
           A2 FFFF
F2 00FF                       end of list

4.2 The replies - one frame per directory entry

Frame 27, frame 46 and frame 62 each carry ONE entry. The second integer steps 0002, 0003, 0004 across them, so it behaves as an entry sequence number. That reading is INFERRED from three consecutive values, not confirmed.

Frame 27:

92 000C
92 0002                       entry sequence
F2 0002  8C 4B                field 2, constructed, 75 bytes:
           A2 0007
           A2 0000
           A2 0001
           B0 40              a 64-byte entry record (see below)
F2 00FF

Frame 46 has the same shape with a 47-byte constructed value holding a 42-byte record, so the entry record is not a fixed-size struct.

4.3 Inside the entry record

Frame 27's 64-byte record, split at the readable boundaries:

90 00
"TXT1" 27                     name, terminated by 0x27
00 00 ... 00                  padding
"SYMB" 07 00 07 00 03 FF 00 10 00 00 00 07 07 00 00 00 00 02
C1 FA E1 F0                   \
C1 FA E2 05                    > three 32-bit values, two of them equal
C1 FA E2 05                   /
00 00 00 01
00 00 00 0C
00 00 78 DA

Frame 46's record, same treatment:

D0 01 02 40 00 00 05 40 00 00
"PACK-ONE" 27                 name, terminated by 0x27
00 00 ... 00
40 00 48 FC  40 00 48 FE  00 00 48 24  00 00 3F D2

VERIFIED: the byte 0x27 terminates a name. It follows TXT1, PACK-ONE and (SECRET) consistently, and ' is the SINTRAN string terminator.

VERIFIED: TXT1 and SYMB correspond to a file name and a file type, matching the SINTRAN name:type convention.

CONFIRMED against FILE-STATISTICS, 2026-07-29

FILE-STAT d100(system).sintran:data run from node 102 printed:

FILE 0 : D100.(PACK-ONE:SYSTEM)SINTRAN:DATA;1
           (ALLOCATED FILE)
           PUBLIC ACCESS : NONE
           FRIEND ACCESS : NONE
           OWN ACCESS : READ, WRITE, APPEND
           OPENED 10 TIMES
           CREATED 16.55.35  JULY 6, 1998
           OPENED FOR READ 16.59.48  JULY 6, 1998
           OPENED FOR WRITE 16.59.48  JULY 6, 1998
           63 PAGES , 122880 BYTES IN FILE

Against the same file's record taken off the wire in a LIST-FILES reply:

Offset Decoded FILE-STATISTICS says
+26 0007 OWN: READ, WRITE, APPEND; PUBLIC and FRIEND none match
+28 0020 (ALLOCATED FILE) match
+38 000A = 10 OPENED 10 TIMES match
+40 1998-07-06 16:55:35 CREATED 16.55.35 JULY 6, 1998 match
+44 1998-07-06 16:59:48 OPENED FOR READ 16.59.48 match
+48 1998-07-06 16:59:48 OPENED FOR WRITE 16.59.48 match
+52 63 63 PAGES match
+56 122879, +1 = 122880 122880 BYTES IN FILE match

Every field matches. The page count, byte count, the three dates and the open count are no longer inferred from internal consistency - they are confirmed against the system's own report, and the +56 = bytes - 1 convention is confirmed exactly. Locked in by Xmsg.Ndfs.Tests.FaDirectoryListingTests.DecodedEntry_MatchesWhatFileStatisticsPrinted.

NOT ESTABLISHED: the remainder. The name does not sit at a fixed offset - it is at +2 in frame 27 and at +10 in frame 46 - so the leading bytes are some kind of field descriptor rather than a header of fixed width. The three equal-looking 32-bit values in frame 27 are consistent with created / last-opened / last-modified timestamps, and 0x78DA is consistent with a byte count, but nothing in this capture tests either reading. Do not build a decoder on those guesses.


5. A second service: *FA-USER

Frames 98 and 106 (client to server) name *FA-USER, distinct from the *FA-SERVER named in frames 1, 130, 156, 190 and 220. The corresponding server frames 92 and 100 carry the literal USER followed by a long zero-filled region, and use a different sub-header (86 84) from the file-access frames (82 84).

This service had not previously been recorded. Its purpose is not established.

Note that this does not by itself imply a remote user-listing capability - see section 6.


6. LIST-USERS: there is no remote form

Checked against the manuals:

  • Reference-Manuals\ND-60.128.5 EN SINTRAN III Reference Manual.md line 6328 gives @LIST-USERS [<directory name>:]<user name>,<output file>. The only qualifier is a local directory, never a system or node.
  • Operations\Cosmos\ND-60.163.4 EN COSMOS User Guide.md appendix G, lines 3593-3632, is the authoritative list of SINTRAN commands that work on remote files. It runs from @ALLOCATE-FILE to @WFILE and includes @LIST-FILES. LIST-USERS is absent, as are LIST-FRIENDS and USER-STATISTICS.
  • The only remote LIST- command in the whole command set is @LIST-REMOTE-QUEUE (same manual, line 5932), which lists a batch queue, not users.

You can list another user's files remotely - ND-60.163.4 line 1108 shows MINOR(TOM(XYZ)).(JERRY)REPORTS:TEXT - but you must already know the user name.

Conclusion: there is no command to enumerate users on a remote machine. The reason the correct command could not be found is that it does not exist.


6a. LIST-SYSTEMS, LIST-ROUTING and LIST-NAMES are three different things

All three were asked about together. They are distinct, and the captures show it.

LIST-SYSTEMS - probes *TADADM on each system

From E:\Dev\Ronny\X25Emulator\pcap\li-syst-tad-103.pcapng, frames 529 and 541, decoded:

41 00 14                      service 0x41 = XSLET, length 20
FF 07 "*TADADM" 00            string parameter 1, padded to a word
FE 04 "D100"                  string parameter 2 - the system being probed
04 02 0001                    integer parameter 4 = 1

Frame 541 is byte-identical except "D102". So LIST-SYSTEMS walks the systems it knows about and sends each one an opening letter addressed to that system's *TADADM. It is a per-system liveness and identity probe, carried out one system at a time over a real server conversation.

This also confirms the general XSLET parameter tagging against a second, independent server: string parameter n is tagged 256 - n (FF = 1, FE = 2), integer parameter n is tagged n, and strings are padded to a word boundary. That rule had only been seen on *FA-SERVER and *XFTRA.

LIST-ROUTING - the XROUT routing table

li-rout-103-tree.pcapng and li-rout-102-tree.pcapng carry no server names at all - no *TADADM, no *FA-*, no *XFTRA. The traffic is short XROUT frames. This is the routing layer answering which link reaches which system; it never opens a conversation with a named server.

LIST-NAMES - the name and port directory

A List-names inside the FILE-TRANSFER program returns, per system, the registered server names and their port numbers. Run live on 2026-07-29 against system 100:

System Port Free SPs Name
100 0 D100.
100 2 *COSPO.
100 3 2 *FA-FSA.
100 4 *TADADM.
100 6 *XM-FIDO.
100 7 1 *XFTRA.
100 11 30 *FA-SERVER.
102 0 D102.
103 0 D103.

This is the port map a server implementation has to register into. Note *FA-USER is absent, consistent with it being the client-side File User registering on the requesting machine rather than a service on 100.

Summary: LIST-NAMES asks what services exist and on which ports; LIST-SYSTEMS asks is this system there, by talking to its *TADADM; LIST-ROUTING asks how do I reach that system.


6b. The FILE-TRANSFER program's advanced mode, and its MON 200 debug trace

SET-ADVANCED-MODE at the F-T: prompt unlocks a second command set. It matters because two of those commands make the original software describe its own protocol:

Mode <File name>,<No of times>
List-variables
Checkout <Remote system and user name>,<No of page transfers>
Decode-buffer <Input buffer (y/n)>
Get-error-message <Error value (dec)>
Debugprint-on
Debugprint-off
Define-transfer-conditions <No of buffers>,<Size in bytes>,<Secure messages>

DEFINE-TRANSFER-CONDITIONS settles what p10 and p11 are

The *XFTRA opening letter carries integer parameters 10 = 1024 and 11 = 2, and both had been recorded as constants of unknown meaning - a controlled-variation run on 2026-07-28 failed to move either. The existence of a command whose only three parameters are number of buffers, size in bytes, secure messages, together with the COSMOS User Guide p.146 statement that "files are transferred using two 1024-byte buffers at a time", makes p10 the size and p11 the count.

Still not proven. No capture yet shows the letter changing when the setting changes. Driving Define-transfer-conditions with different values and re-capturing would prove it. The third knob, "secure messages", has not been located in the letter at all.

DEBUGPRINT-ON traces the MON 200 calls

With debug printing on, each XMSG call is printed with its registers before and after. From a Checkout d102(system),2 on node 100, 2026-07-29 (identical across three consecutive runs):

* XMSG Function:     2. Regs (A,D,X):      200      47   54457
   Return status=     1                  161605      47   54457
* XMSG Function:    12. Regs (A,D,X):        0      47       0
   Return status=     1                       5      47       0
*** Error in accessing: DUMMY
Sintran file system error:
NO SUCH FILE NAME
* XMSG Function:     1. Regs (A,D,X):        5      47      34
   Return status=     1                       5      47      34

The numbers are OCTAL. The A register is 16 bits, and 161605 decimal does not fit; as octal it is 58245, which does.

The function numbers decode against Xmsg.Protocol.Enums.XmsgFunction:

Printed Function Meaning
2 XFGET get a message buffer
12 XFSND send
1 XFDCT disconnect / release

So the shape is get buffer, send, release. D is 47B = 39 on every call including the returns, so it behaves as a message or port handle held across the sequence. The value XFGET returns in A, 161605B, has the shape of a message identifier.

XFSND returned success and NOTHING crossed the wire - the capture over both HDLC links for the same period contains zero packets.

CORRECTION. This document first explained that as "the send was a local XROUT lookup, because XROUT on node 100 already knows D102 from its routing table". That explanation is wrong. A later capture on the same day shows a remote server lookup DOES cross the wire: a CREATE-FILE d102(system).dummy:data,2 on node 100 produced 8 frames carrying *FA-SERVER and D102 between 100 and 102, ending in "REMOTE FILE SERVER IS NOT AVAILABLE". Resolving a named server on a REMOTE system is a network round trip.

What the Checkout trace actually shows is only that whatever that XFSND was, it was not a remote server lookup - the local DUMMY open failed before any remote resolution was attempted. Its destination is NOT established.

It also corrects an assumption: Checkout does not open its network conversation before touching the file. It does a local lookup, opens the file, and only then would the conversation follow. The local file open is the gate - which is the same trap that made four of five TRANSFER-FILE attempts look like network failures on 2026-07-28.

Checkout needs a scratch file called DUMMY. Creating DUMMY:DATA (2 pages) under (PACK-ONE:SYSTEM) on node 100 did not satisfy it - the same error recurred. Where the file must live, and with which type, is NOT established. A remote DUMMY on d102 has not been tried.

CORRECTION to PLAN-FILE-SERVER-CAPTURE-2026-07-28.md

That document states that on E02 the commands DEBUGPRINT-ON, DEBUGPRINT-OFF, DECODE-BUFFER, CHECKOUT, LIST-VARIABLES and DEFINE-TRANSFER-CONDITIONS all answer ** Illegal command **, and concludes: "So there is no built-in buffer dump to lean on - scratch the plan above that hoped for one. The MON 200 trace is the instrument."

That conclusion is wrong. All of those commands exist on E02. They are gated behind SET-ADVANCED-MODE, which promotes the prompt from F-T: to F-T(Adv.):. Every one of them was run successfully on 2026-07-29 against (COSMOS-BASIC)COS-FILE-TRA-E02:PROG on node 100.

The practical cost of the error was real: it closed off the program's own decoder and variable dump as instruments for a day.

LIST-VARIABLES - the program's own state

Name buffer contents:

Input buffer address: 54216
Output buffer address: 54316
Page buffer address: 216
Function (oct): 0, page no: 0, displacement: 0
Letter status (oct): 0, remote file system status (oct): 0
Number of transfer buffers: 2 of size: 1024 bytes.

This CONFIRMS p10 and p11. The *XFTRA letter carries integer parameter 10 = 1024 and parameter 11 = 2; the program reports "2 buffers of size 1024 bytes". Parameter 10 is the buffer size and parameter 11 the count. This was previously recorded as inferred - the controlled-variation run on 2026-07-28 never moved either field - and can now be treated as established, although a capture showing the letter change when DEFINE-TRANSFER-CONDITIONS changes would still be stronger.

Function, page no and displacement are the bulk-transfer request fields, held as live program state. This is the first direct evidence of the structure that COSMOS-XMSG-Synthesis.md section 8 predicted from disassembly ("the request/reply carry position + count as typed params"). It also shows the transfer is addressed by page number plus displacement, not by a byte offset alone.

Two separate status words are tracked - Letter status and remote file system status - which matches the wire behaviour where a refusal replaces the service byte (letter level) while a file error arrives as a tagged value in the body (file-system level).

GET-ERROR-MESSAGE 48

Error code not recognised by XMSG error routine

Useful negative evidence. The value 48 decoded off the wire in a rejected file access is not an XMSG error code, which is consistent with reading it as the SINTRAN file-system error 060 octal, "Wrong password". It does not by itself prove that reading.


6c. A COMPLETE file transfer, captured

Capture: E:\Dev\Ronny\X25Emulator\pcap\claude-transfer-file-COMPLETE-102-to-100-2026-07-29.pcapng

This is the first capture anywhere of COSMOS file content crossing the wire. Every earlier attempt caught only the opening letter.

Command, run on node 102 inside COS-FILE-TRA-E02 with DEBUGPRINT-ON:

TRANSFER-FILE d100(system)."xfertest:data",DUMMY:DATA

Result: Completed. Transfer rate: 3 Kbytes/sec, and XFERTEST:DATA created on node 100.

The destination must be QUOTED to be created

The identical command with an unquoted destination fails with:

*** Error in accessing: D100(SYSTEM).XFERTEST:DATA
Sintran file system error: NO SUCH FILE NAME

TRANSFER-FILE does not create the destination implicitly. The quotes are what the remote reads as "create this file". This confirms, from the client side, the note already recorded against the captured *XFTRA letter - that the quotes around "XMSG-COPY:BATC" are content the client must not strip.

Note the remote spec needs a dot between the system/user part and the file name: d100(system).name:type. Without it SINTRAN answers ILLEGAL CHARACTER IN PARAMETER.

Wire measurements

Measure Value
Payload frames 45
Frames of exactly 256 bytes 16
Total payload bytes 102 -> 100 4601
Total payload bytes 100 -> 102 504

CORRECTION - the 256-byte figure is NOT the LAPB frame size. This section first concluded "the on-the-wire unit is a 256-byte LAPB I-frame". Wrong. 256 is the TCP segment size of the bridge carrying HDLC over TCP. Checking which payloads actually begin with the 0x7E flag shows frames 15, 17 and 19 do not - they are TCP continuations of the single LAPB frame that started at frame 13.

SECOND CORRECTION - it is not 1024 either. The estimate "roughly 1024 bytes of payload, matching the 1024-byte buffer" was arithmetic on TCP segments again, not on reassembled frames.

Proper reassembly through HdlcPcap.ReadFrames (see Xmsg.Protocol.Tests/FileTransferStreamTests.cs) gives the real figures:

Direction Information frames Info bytes
102 -> 100 16 4496
100 -> 102 12 352

Information-field sizes are 450 and 622 bytes, four of each, plus smaller control-carrying frames - not a uniform 1024, and not 256. Total LAPB frames: 39.

The relationship between the 1024-byte application buffer and the 450/622-byte frames is NOT established. Note only that 4 x 450 + 4 x 622 = 4288, against a 2-page (4096-byte) file, leaving 192 bytes across 8 frames - consistent with a per-frame header of about 24 bytes, but that is arithmetic, not a decode. Do not build on it.

The LAPB control bytes are decisive. Data frames carry I-frame control values (0xAA, 0xEE, 0x26, 0x6C - bit 0 clear), and the reverse direction carries RR supervisory frames (0xC1, 0x81, 0xA1, 0x01, 0x21, 0x61 - bits 1:0 = 01), each with a receive sequence number N(R).

Acknowledgements arrive interleaved with data rather than gating it: data at t=1.97, an RR at t=2.02, more data at t=2.03. So the sender does not halt for each acknowledgement.

ESTABLISHED - there IS application-level reply traffic, not only link acknowledgement. Node 100 sends 12 information frames totalling 352 bytes back, alongside 10 supervisory RR frames. Had the reverse direction been acknowledgement only, it would carry supervisory frames and nothing else. So the transfer is a request/reply exchange at the XMSG layer, riding on LAPB's acknowledged link - which matches the XFRCV after each buffer in the MON 200 trace.

NOT established - and one measurement is known bad:

The transfer message, DECODED

Dumped by FileTransferStreamTests.TransferStream_DumpInformationFieldHeads.

Every information field opens with the SINTRAN header 2113 <type> <src system> <dst system> <counter> <length> <checksum>, with the systems appearing as 0064 (100) and 0066 (102). Data frames then carry an XMSG sub-header, and the body follows.

Across the whole transfer the bodies are:

0406  0042 0000 0000   + 1024 bytes    page 0, displacement 0
0406  0042 0000 0200   + 1024 bytes    page 0, displacement 512
0406  0042 0001 0000   + 1024 bytes    page 1, displacement 0
0406  0042 0001 0200   + 1024 bytes    page 1, displacement 512
0006  0043 ffff ffff                   end of transfer

The arithmetic closes exactly. The declared length 0x0406 is 1030, and 1030 - 6 = 1024 - the six header bytes are the function, the page number and the displacement, and the remainder is one full buffer of file data. That 1024 is the same number LIST-VARIABLES reports as the buffer size and the same number carried as integer parameter 10 of the *XFTRA letter.

Field Width Observed
function 2 bytes 0042 for a data block, 0043 for end of transfer
page number 2 bytes 0, then 1 - the file is 2 pages
displacement 2 bytes 0 and 0200
data 1024 bytes the file contents

Displacement is counted in WORDS, not bytes. 0x200 is 512, and 512 words is 1024 bytes - exactly one buffer, so the two blocks of a page are at displacement 0 and 512. A SINTRAN page is 1KW = 2048 bytes, so each message carries half a page. Two pages x two halves = the four data messages seen.

The end marker 0043 carries ffff ffff and a declared length of 6, so it has a header and no data.

This also disposes of the 450/622-byte question. Those are not protocol blocks: LAPB fragments the single 1030-byte message across two frames because its own frame limit is about 600 bytes. Nothing at the XMSG layer is 450 or 622 bytes long.

The reverse direction carries a 34-byte frame per block, echoing the length 0406 followed by zeros, and a final one carrying 0006 0000 0000 0001. These are the application-level replies established earlier - one per data block.

Two incidental confirmations from the same dump: the opening letter is visible as 0141 003a ff06 2a58 4654 5241 fe04 4431 3030 - XSLET, *XFTRA, D100 - and node 100's first reply carries the string XFERTEST', the name of the file it was asked to create.

Caveat, now resolved. The first transfer (DUMMY:DATA) was a freshly created file and therefore all zeros, so nothing proved the data region was a plain byte copy. A second capture settles it.

A content-bearing PULL

Capture: E:\Dev\Ronny\X25Emulator\pcap\claude-transfer-PULL-content-100-to-102-2026-07-29.pcapng

TRANSFER-FILE "ftpull:symb",d100(system).filsys-symbols:symb
  -> 21 pages, page index 0..20, Completed. Transfer rate: 4 Kbytes/sec

The data region is a plain byte copy. The payloads contain readable text - BANK, SN, octal digit runs - which is exactly what the :SYMB symbol-table file holds. Nothing is compressed, encoded or reordered.

PULL and PUSH letters DIFFER - a correction

XMSG-XFTRA-CONTROLLED-VARIATION-2026-07-28.md recorded, from push captures, that the source file is not in the request at all. That holds for a push, but is not a general rule. The pull letter is:

0141 003E  FF06 "*XFTRA"  FE04 "D100"  F406 "SYSTEM"  0D02 0000
           F813 "FILSYS-SYMBOLS:SYMB"

Parameter 8 carries the remote file name in both directions. On a push that is the destination; on a pull it is the source. So parameter 8 is "the file on the other machine", and the local file never appears in the letter either way. That is a cleaner rule than the one recorded before, and it explains the earlier observation without contradicting it.

The pull reply carries the source file's object entry

Node 100 answers with a record beginning 9000, the name FILSYS-SYMBOLS', the type SYMB - the same 64-byte object entry structure returned by LIST-FILES. Two further field confirmations come free:

  • +28 reads 0008 here against 0020 for SINTRAN:DATA, matching FILE-STATISTICS reporting "(INDEXED FILE)" for this file and "(ALLOCATED FILE)" for that one.
  • +38 reads 0003, one more than the "OPENED 2 TIMES" that FILE-STATISTICS reported minutes earlier - the transfer itself being the third open.

Acknowledgement cadence, measured

Measured by FileTransferStreamTests.TransferStream_IsTheSendWindowActuallyUsed.

An earlier attempt walked the capture counting frames sent but not yet acknowledged and reported 16 outstanding. That was wrong and its numbers must not be quoted - LAPB's modulo-8 space allows at most 7. The cause: HdlcPcap.ReadFrames returns frames grouped by directional flow, not interleaved in capture order, so no acknowledgement ever falls between two data frames.

Order within a flow is preserved, so the N(R) progression is reliable:

Direction N(R) sequence Largest advance
100 -> 102 (acknowledging the file data) 4,4,5,6,7,7,0,1,2,2,3,4,5,5,6,7,0,0,1,2,2,3 1
102 -> 100 (acknowledging the replies) 3,4,5,5,5,7,7,7,1,1,1,3,3,3,5,5,7 2

The bulk data direction is acknowledged one frame at a time. Every N(R) advance is exactly 1 across all 22 acknowledgements, with no exceptions. The reverse direction advances by 2 on five occasions, so the sliding window is genuinely live on this link - it simply is not exercised for the file data.

What this does NOT prove. Per-frame acknowledgement is a property of the RECEIVER. A sender with two frames in flight could still be acknowledged individually, producing the same N(R) trace. To establish in-flight depth the capture must be walked in true interleaved order, which HdlcPcap.ReadFrames does not currently support - it would need frames tagged with the capture ordinal of the segment they end in. So "the data direction is not pipelined" is consistent with the evidence but not established by it.

This resolves the contradiction flagged earlier between COSMOS-XMSG-Synthesis.md section 8 (~0x800-byte page buffer) and COS-FA-SERV-E04-Analysis.md section 6.1 (1850 words = 3700 bytes): both describe application-side buffers, neither describes the wire unit. Nothing on the wire carries a 2048- or 3700-byte block.

The reverse direction carried 504 bytes in total against 4601 forward, so acknowledgements are small and far fewer than one per data frame.

The MON 200 call sequence of a real transfer

Per page, the pattern is:

10  XFOPN   A=<magic>          open
 7  XFWRI   A=216 then 1216    write - the two addresses are 1000 octal apart, one buffer
 5          A=102, X=0 / 1000  displacement 0 then 1000 octal = 512 words = 1024 bytes
1014        (XFSND + options)  send

then the reply side:

40015       (XFRCV + wait)     status 0 = nothing yet, status 1 = message present
 4          A=177777           consume

Current page index: printed 0 then 1, matching two pages.

Functions 3, 4, 5 and 11 are NOT identified against XmsgFunction. Function 5 carries the displacement and function 4 always takes A=177777 (-1), but neither is confirmed. The composite values 1014, 23014 and 40015 are octal and clearly a function number OR'd with option bits - 40015 and 23014 both end in a low nibble matching XFRCV and XFSND respectively - but the exact split has not been verified against XmsgOption.


6d. FILE-STATISTICS on the wire, and the 8000 0001 opener SOLVED

Capture: E:\Dev\Ronny\X25Emulator\pcap\claude-file-stat-102-to-100-2026-07-29.pcapng (FILE-STAT d100(system).sintran:data from node 102).

The session opens with the documented FA-SERVER letter, byte for byte as previously recorded:

1B 41 0012  FF 0A "*FA-SERVER"  FE 04 "D1.."

A 4-byte session header sits in front of every QFORM body

Earlier documents flagged an unexplained 80 00 00 01 at the start of a body and listed it under "still open"; a later note in this file dismissed it as an artifact of reading from the wrong offset. Both were wrong. It is real, and this capture shows what it is, because six requests occur in one session and the field varies systematically:

07F0 0046   8000 0001   92 000C ...    first request
07F0 0046   8100 D761   92 000C ...    second
07F0 0046   8200 D761   92 000C ...    third
07F0 0046   8300 D761   92 000C ...    fourth
07F0 0046   8400 D761   92 000C ...    fifth
07F0 0046   8500 D761   92 000C ...    sixth

The four bytes before the QFORM body are a sequence byte 0x80 + n, a zero byte, and a 16-bit token. The token is 0x0001 on the opening exchange and a constant 0xD761 for the remainder of the session, which is the shape of a value handed out by the server on first contact and echoed thereafter. That reading of the token is INFERRED - nothing here shows the server choosing it.

This also explains why a body offset of 38 works: it is 34 bytes of SINTRAN and XMSG headers plus these 4. The QFORM body proper begins after them, which is why every body appears to start 92 000C.

The value 07F0 precedes a request carrying a QFORM body; 07A2 appears instead on short 36-byte frames whose payload is 0100 8485, 0200 8485, 0300 8485 and so on - an incrementing counter with a constant. Their role is not established.

Field 1 of the first request carries A2 07D0 = 2000, matching what was recorded from the earlier password capture.


6e. DELETE-FILE, and the message-type / conversation fields

Capture: E:\Dev\Ronny\X25Emulator\pcap\claude-delete-file-102-to-100-2026-07-29.pcapng (DELETE-FILE d100(system).xfertest:data from node 102 - the file created by the earlier transfer).

It completes with no confirmation prompt and takes three QFORM exchanges:

07F0 0048   8000 0001   92 0002  92 0001  F2 0001  A2 07D0
07F0 0048   8100 D761   92 000B  92 0002  F2 0001  BF "XFERTEST:DATA.."
07F0 0048   8200 D761   92 0003  92 0003  F2 00FF

The file name arrives under selector 1 as a BF byte string - class 3, length 15 - and the third exchange is a bare end-of-list. The first exchange is byte-identical to the first exchange of FILE-STATISTICS, including A2 07D0 = 2000, so the opening handshake is shared across operations.

The word after the message tag is a conversation number, not an opcode

Comparing the three captured operations:

Operation Value
LIST-FILES 003F
FILE-STATISTICS 0046
DELETE-FILE 0048

It increases in the order the captures were taken and never repeats, so it identifies the conversation rather than the operation. This matches the earlier observation in XMSG-FA-SERVER-REQUEST-CAPTURED-2026-07-28.md that a trailer byte "behaves like a request counter incrementing by 2, not a function code" - the same counter, seen in a different place.

So the operation is NOT carried in this field. What distinguishes DELETE-FILE from FILE-STATISTICS on the wire is the sequence of QFORM exchanges and the selectors used, not an opcode byte. That is consistent with COSMOS-XMSG-Synthesis.md section 8, which records that dispatch in the server is decentralised with no opcode-to-handler table.

Message-type tags

Four tags appear in front of that conversation number:

Tag Seen carrying
07F0 a request with a QFORM body
07A2 a short 36-byte frame, body 0100 8485, 0200 8485, 0300 8485 - a counter and a constant
0782 0002 8000 0000
07C0 / 07D2 the closing exchange; the reply carries 6400 = 100, the system number

The 07A2 counter runs in step with the exchange number. Beyond that, the meanings of these tags are NOT established.


6f. CORRECTION: a transfer message is SPLIT ACROSS TWO LAPB FRAMES

Section 6c described the transfer message as a single 1030-byte unit. On the wire it is not. It is segmented into two LAPB information frames, and the segmentation is visible in the SINTRAN header:

2113 000a 0064 0066 0178 0406 ...   len=622   first fragment
2113 000c 0064 0066 0178 0252 ...   len=450   continuation

Two packet subtypes appear here that are not in SintranPacketSubtype, which only knows 0x0E for data:

Subtype Role Header before the body
0x0A first fragment of a segmented message 28 bytes: SINTRAN header + full addressing
0x0C continuation 14 bytes: SINTRAN header + the counter byte only

A continuation does not repeat the addressing words - it carries the 13-byte SINTRAN header plus the single counter byte and then picks the message straight back up. The two fragments are paired by the Flags1 datagram sequence, which is identical on both.

Flags2 on the two fragments

Fragment Flags2 Meaning
0x0A 0406 = 1030 total message length
0x0C 0252 = 594 byte offset at which the continuation resumes

VERIFIED on all four messages in claude-transfer-file-COMPLETE-102-to-100-2026-07-29.pcapng: 594 + 436 = 1030, exactly, every time.

CORRECTION: the message body starts at info-field offset 28, not 32

This is what made the first attempt at the arithmetic above miss by four bytes.

XmsgSubHeader documents a 19-byte sub-header whose offsets 13-16 are a four-byte XMCSM control word, which would put the body at 13 + 19 = 32. Measured against the corpus, only the HIGH half of XMCSM is a header field - and it is simply Flags2 repeated. That is why the long-standing rule Flags2 == XMCSM >> 16 has always held: they are the same field written twice.

The LOW half of XMCSM is already the first word of the message body. So:

info off 0   SINTRAN header                        13 bytes
info off 13  counter, 21 00 marker, flags, role,
             XMDSY / XMDPT / XMSSY / XMSPT         15 bytes
info off 28  message body starts here

(Add 2 for the frame-relative offsets used in section 2: body at frame offset 30, which is exactly where section 2 shows 07 F0 00 02 beginning.)

This makes the message-type word (07F0, 07A2, 0782, 07C0, 07D2) and the transfer function code (0042) the same field - the first word of every message body - rather than two unrelated discoveries. Body layout, unified:

QFORM message:  <type 07F0> <conversation> <4-byte session header> <QFORM ...>
transfer:       <fn 0042>   <page>         <displacement in words> <1024 bytes>

6 + 1024 = 1030. The displacement advances 0 -> 512 and resets as the page advances, which is what establishes that it counts words and that one page holds two 1024-byte blocks.

For the QFORM classes, Flags2 is exactly the body length

VERIFIED across 98 data frames in three captures - FILE-STAT (28), DELETE-FILE (17) and LIST-FILES (53), both directions - with zero mismatches:

Flags2 == (information field length) - 28

Two classes in the transfer capture do NOT follow it, and both are real message classes rather than noise:

  • Class 0x0080 - the two XSLET opening letters that name *XFTRA in plain ASCII. Here 0x0080 is a class code, not a length.
  • The transfer acknowledgements - 34-byte frames whose body is six zero bytes, but which carry the acknowledged message's XMCSM verbatim, so Flags2 reads 1030.

So Flags2 is a frame-CLASS word, and "body length" is what that word means for the QFORM request/reply class specifically.

ANSWERED: the transfer is STOP-AND-WAIT at the application layer

Four data messages, four acknowledgements - but a 1:1 count says nothing about ordering, so this was settled separately by walking both directions in the order the frames completed on the wire:

sent message 1 (outstanding now 1)
acknowledged 1 (outstanding now 0)
sent message 2 (outstanding now 1)
acknowledged 2 (outstanding now 0)
sent message 3 (outstanding now 1)
acknowledged 3 (outstanding now 0)
sent message 4 (outstanding now 1)
acknowledged 4 (outstanding now 0)

PEAK OUTSTANDING: 1

Peak outstanding is 1. The sender never starts a message before the previous one is acknowledged - strict alternation, no pipelining. This is the answer to "is the transfer using a sliding window ack, or waiting for ack": the LAPB layer underneath has a sliding window and uses it (a 1030-byte message is two I-frames), but the file-transfer application above it waits for every message.

That matters for a server implementation: it may answer one message at a time and never needs to buffer a window of outstanding transfer blocks.

How this was measured. HdlcPcap.ReadFrames reassembles each TCP flow separately and returns the flows one after another, so the two directions are never interleaved and no ordering question can be answered from it. A new HdlcPcap.ReadFramesInCaptureOrder places each frame using the capture ordinal of the TCP segment carrying its closing flag. A guard test asserts both readers return the same 39 frames, so the ordering conclusion cannot be drawn from a stream that silently lost or duplicated frames.

This is the third time flow-grouping has nearly produced a wrong published claim - it previously yielded an impossible count of 16 outstanding LAPB frames. Never answer an ordering question with ReadFrames.

The complete transfer, message by message

Every body in the transfer, in wire order, dumped from the capture rather than reconstructed:

102->100  62  0141 003a ff06 *XFTRA fe04 D100 f406 SYSTEM 0d02 0000
              f80f "XFERTEST:DATA" 00 f704 SYMB 0a02 0400 0b02 0002
100->102  70  0100 0000 ff06 d000 XFERTEST' ... DATA <object entry>
102->100 1030 0042 0000 0000 <1024 bytes>      \
100->102    6 0000 0000 0000                    | four times, strictly
102->100 1030 0042 0000 0200 <1024 bytes>       | alternating
100->102    6 0000 0000 0000                   /
102->100    6 0043 ffff ffff                   end of transfer
100->102    6 0000 0000 0001                   completion

Points worth recording:

  • The opening letter is an XSLET letter with the parameter tagging already known from XSLET: FF = string parameter 1 (*XFTRA, the server name), FE = parameter 2 (D100), F4 = SYSTEM, F8 = the quoted file name XFERTEST:DATA, F7 = SYMB.
  • The opening reply carries a 64-byte object entry for the file just created - the same structure the directory listing returns. So the transfer's opening exchange hands back the created file's directory entry.
  • The end-of-transfer reply is 0000 0000 0001, differing from the four data replies (0000 0000 0000) in the final word only. Whether that word is a status or a count is UNKNOWN - one capture cannot distinguish them.

The codec is E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol\Fa\FaTransferCodec.cs, held to these bytes by E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol.Tests\FaTransferCodecTests.cs: every message it builds is byte-identical to the captured one, and the split point it computes matches the segmentation the machine used.

One caveat on the 594-byte split. It is identical on all four messages, but only one transfer has ever been captured. Whether 594 is fixed by the protocol or falls out of this link's frame size is UNKNOWN. A transfer of a differently sized file would settle it.

Tests: E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol.Tests\TransferFragmentationTests.cs (6 tests). Library support added in E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Hdlc\HdlcPcap.cs and E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Hdlc\HdlcDeframer.cs.


6g. The operation code, and a CORRECTION to FaListFilesCodec

Dumping FILE-STATISTICS and DELETE-FILE side by side - in wire order, complete bodies rather than 48-byte heads - settled how an operation is identified.

The opening request of the two operations is byte-identical apart from the conversation number. Asserted, not eyeballed: 112 bytes, zero differences outside offsets 2-3.

FILE-STAT    07F0 0046  8000 0001  92 0002 92 0001 ...
DELETE-FILE  07F0 0048  8000 0001  92 0002 92 0001 ...
                  ^^^^ conversation number only

So the operation is not named in the opening exchange at all, which confirms the earlier conclusion that the word after the message type is a conversation counter rather than an opcode.

Where the operation IS named

The first QFORM field of each exchange is an operation code, and the second is the exchange sequence. Both are echoed by the reply, which is what matches a reply to its request.

Exchange DELETE-FILE FILE-STATISTICS Payload
1 92 0002 92 0002 pack and user - BAK05 SYSTEM, then a 56-byte block holding SYSTEM'
2 92 000B 92 000C delete: the file name XFERTEST:DATA; stat: the enquiry block
3 92 0003 92 0003 close

92 000B in exchange 2 is what makes a delete a delete. Codes so far:

Code Meaning
0002 open, carrying the directory and user spec
000B delete
000C directory or file enquiry (LIST-FILES, FILE-STATISTICS)
0003 close

The session header also resolves cleanly: its first byte is 0x80 + n counting exchanges from zero, and the token is 0001 on the first exchange, then D761 from the asker and 9081 from the responder.

CORRECTION to FaListFilesCodec

That codec declared:

public const ushort LeadingConstant = 0x000C;   // "Meaning UNKNOWN"

It is not a constant - it is the operation code, and it only looked constant because every frame examined at the time belonged to one operation. Renamed to OperationDirectoryEnquiry and now sourced from FaExchangeCodec. This is the second time in this work that "constant on every captured frame" has meant "only one case was captured"; the first was the 594-byte fragment split, which is still flagged UNKNOWN for the same reason.

New code: E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol\Fa\FaExchangeCodec.cs, tested by E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol.Tests\FaExchangeCodecTests.cs. The body dump used to derive it is E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol.Tests\FaOperationDumpTests.cs.


6h. Client and server drivers, verified by replay

Both halves of a file-server conversation are now driven by code, and both are held to the captures by replay: the client driver rebuilds every request the real client sent, and the server driver rebuilds every message node 100 answered with - byte for byte, in both cases.

FaClientConversation FaServerConversation
Conversation number its own (003F, 0044, 0046, 0048 ...) always 0002
Session token 0001 on exchange 1, then D761 9081 on every exchange
Session counter 0x80 + n 0x80 + n
Short-ack constant 8485 922A

The token asymmetry is the trap. A server written symmetric with the client would send 0001 on its first reply; the capture says 9081 from the very first one. There is a test whose only job is to pin that difference.

Conversation lengths differ by operation and are NOT a fixed three:

  • DELETE-FILE: open (0002), delete (000B), close (0003) - 3 exchanges.
  • FILE-STATISTICS: open, then the enquiry 000C four times, then close - 6 exchanges.

The server also sends three messages that are not replies: the connection confirmation 07D2 0002 <client conversation> 6400 - the one server message carrying the CLIENT's conversation number, and the system number 100 - the short acknowledgements, and the close 07C0 0002 <client conversation> 0000, which both sides send identically.

Structural resemblance to RR-LIB - a lead, NOT a finding

The conversation looks like the request-response model in Xmsg.Api.Rr: an opening letter carrying user data that names the server, a confirmation, request/response pairs, then a disconnect. No document has been found stating that *FA-SERVER is an RR-LIB server. The mapping is inferred from shape alone and is recorded as a lead worth chasing, not as established fact.

Code: E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol\Fa\FaClientConversation.cs, E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol\Fa\FaServerConversation.cs. Tests: E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol.Tests\FaClientConversationTests.cs, E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\SRC\Xmsg.Protocol.Tests\FaServerConversationTests.cs.


7. What is still needed

To build a server that answers real clients, the following are not yet captured:

  1. FILE-STATISTICS against a known file, so the entry record can be read against known values (size, dates) instead of guessed at.
  2. CREATE-FILE with a stated page count, which should pin the create op and its parameters.
  3. A listing of a directory with many files of known sizes and dates, which is what would actually settle the record layout in section 4.3.
  4. A bulk file transfer that completes. No capture of file content on the wire exists at all - only the opening letter. The block size, position/count fields and the acknowledgement rule are known from disassembly only.
  5. DELETE-FILE, and a read and a write against an open file.

6i. The spec block across two users, and a third "constant" that was not one (2026-07-30)

Two of the values the listing codec hard-codes were checked against a SECOND directory, which needed no new capture - the two listings already on disk are against two different users on node 100, SYSTEM and SECRET.

What survived

Value First case Second case Verdict
Selector-1 value 0x0078 SYSTEM, 11 requests SECRET, 2 requests VERIFIED across two users
Spec block length 62 SYSTEM SECRET VERIFIED across two users

Neither is a single-case artefact any longer. Note the one thing this pair cannot settle: SYSTEM and SECRET are both six characters, so a length that happens to track the user name length would look constant here too.

What did NOT survive: "byte-identical from one entry to the next"

FaListFilesCodec documented the request body as byte-identical across a listing apart from the serial and the cursor. Measured over all eleven directory enquiries in the SYSTEM capture, that holds only from the 0xFFFF request onward. The enquiry BEFORE it differs:

request 0   cursor 002d   ...4d29 2927 3704 b5dc 0000 0000 0000 48ff 0000 0000 ... 0000 0000
request 1   cursor ffff   ...4d29 2927 3704 b5dc 2704 0000 2705 48ff 0000 ffff ... 0540 b56b
requests 2-10  cursors 1..9, block byte-identical to request 1

Same leading filespec text, but a zero-filled tail where the walk requests carry values. So part of the 62-byte block is state that exists only once the walk has started, not something the caller supplies for the whole listing.

Meaning UNKNOWN. It is consistent with a position handle carried forward from the previous reply - 0x2704/0x2705 look like terminated small values and 0xFFFF matches the cursor of the request it appears in - but nothing in either capture tests that reading. The block stays opaque and must be replayed, never synthesised.

The block's leading text, both users

SYSTEM   (SYSTEM)'EM).(SYSTEM)'         then 3704 b5dc ... 48ff ... zero fill
SECRET   (SECRET)'ET(SECRET)).(SECRET)' then 0000 ... 48ff ... zero fill

23 of the 62 bytes differ between the two users. 0x48FF sits in the same relative position in both. The text is truncated on the left - the SECRET block still shows the password parentheses that were typed, the SYSTEM one does not - so this is a fixed-width window over the typed specification, not a self-describing string. Where the window starts is UNKNOWN.

Cursor sequence, now asserted

0xFFFF asks for the first entry, then the cursor runs 1, 2, 3, ... one per entry. The SYSTEM walk is ffff then 1..9 for ten entries; the SECRET walk is ffff then 1 for two.

Locked in by Xmsg.Protocol.Tests.FaSpecBlockCrossUserTests (SpecBlock_IsSameShapeButDifferentContentForTwoUsers, EntryWalkRequests_ShareOneSpecBlock).

Methodological note, for the fourth time

This is the fourth "constant on every captured frame" in this decode that turned out to mean "only one case was captured": LeadingConstant (was the operation code), the 594-byte fragment split (one file size), and now the per-listing spec block. RequestSelector1Value and the 62-byte length are the two that DID survive - and they only count as surviving because a second case was actually run against them.


6j. Why the envelope channel rule failed on this traffic (2026-07-30)

EnvelopeConformanceTests.AllCaptures_EnvelopeModel_ReproducesEveryDataFrameChannel had been red at 70 mismatches out of 970 ever since the file-server captures joined the corpus. The cause is now identified, and it is caused by the very finding in section 6f.

The rule and its hidden assumption

baseLow = (seed - (Flags2 & 0xFF)) & 0xFF
epoch   = (Flags1 - baseLow + 0xFF) >> 8
Channel = 0xDE - (XMCSM >> 24) - epoch

Flags2 & 0xFF was a message-class marker in every capture the rule was derived from: 0x00 for control, 0x08 for terminal data. On COSMOS file-server traffic Flags 2 is the message body length (length - 28). So baseLow becomes a function of how long the message happened to be, and the epoch derived from it is a wrap count of nothing.

The measurement

Split the whole corpus by that one property:

Flags 2 low byte Data frames Channel mismatches
a class marker (0x00 / 0x08) 800 0
a body length 170 70

The rule is not weakly true everywhere. It is exactly true on 800 frames and simply does not apply to the other class. Every one of the 70 was the channel off by 1 or 2 with the counter correct - and the counter agreeing proves nothing, because the test learns the seed from the same frame, which makes LearnSeed its own inverse.

What was changed, and what was not

  • The conformance test now asserts the domain where the rule holds and counts the rest without asserting on it. The whole suite is green: 439 passing, 0 failing.
  • XmsgEnvelope carries the scope limit next to the rule itself.
  • ChannelOffsetDiagnosticTests locks the 800/0 versus 170/70 split in, so a future change that quietly widens or narrows the domain fails.

No replacement formula was fitted. The wire channel offsets on length-valued Flags 2 run 0, 1, 2 and 3. Three frames were worked through by hand and a rule could be made to fit them, which is exactly the mistake section 6i is about. Until that class is captured in quantity, the honest options are to track the channel per direction or to leave it undeclared.

Also worth stating plainly: the Counter line of the model survives on all 970 frames, so only the channel derivation was ever class-dependent.


6k. Single-action recordings: which service does what (2026-07-30)

Three operations had been seen but not attributed, because every earlier recording contained several operator commands and a disconnect. Two recordings were then made with exactly ONE command each, and the recording stopped BEFORE the session disconnected, which is what makes the attribution sound.

The split

operator command service exchanges on the wire
OPEN-FILE *FA-SERVER 0002 open spec, then 0005 open file. Nothing else.
CLOSE-FILE *FA-USER no *FA-SERVER traffic at all
disconnect / logout *FA-SERVER 0006, an 000C, then 0003

What this settles

The conversation is long-lived. claude-OPENONLY-102-to-100-2026-07-30.pcapng holds exactly two exchanges and no close of any kind. The conversation is left standing while the file is open, so it spans the file's lifetime rather than one command. A client that tears the conversation down after each request is wrong.

Operation 0x0006 is not the file close. It had been recorded as "consistent with closing the file"; that was wrong. It only appears when the terminal session ends. What it does is still UNKNOWN, but its trigger is now narrowed to session teardown.

*FA-USER has a purpose. Recorded on 2026-07-29 as a second service whose "purpose is not established". claude-CLOSEONLY-102-to-100-2026-07-30.pcapng contains only this, in a window whose sole operator command was CLOSE-FILE:

100->102  len=100   0144 0060 0104 0066 0587  "USER" then zero fill
102->100  len=14    0100 000a fe08 "*FA-USER"

So closing a remote file is handled by *FA-USER, not by the file-access server. Both frames still need decoding.

Attribution caveat. The close window also contained the login sequence. Login on its own has never produced file-access traffic in any earlier recording - the *FA-SERVER opener has always appeared only when a file command was typed - so CLOSE-FILE is the reasonable attribution. It is not proven to the standard of the open recording, where the command was the only thing in the window.

Method note

The trick that made these work: give dumpcap a duration short enough that the recording ends while the terminal session is still connected. Stopping the recorder by hand always came after the disconnect, and the disconnect generates its own traffic, which is exactly what had been polluting the earlier attributions.

6k.1 The two *FA-USER frames, partially decoded

Raw information fields from claude-CLOSEONLY-102-to-100-2026-07-30.pcapng, the recording whose only operator command was CLOSE-FILE:

100->102  len=128  2113 000e 0066 0064 0399 0064 da17 2100 8684 0066 0000 0064 0605 0064
                   0144 0060 0104 0066 0587 5553 4552 <86 zero bytes>

102->100  len=42   2113 000e 0064 0066 03a0 0064 da10 2100 8660 0064 0605 0064 0605 0064
                   0100 000a fe08 2a46 412d 5553 4552

What is established:

  • Both are ordinary subtype 0x0E data frames with the usual 13-byte SINTRAN header, so the transport is the same as everything else. Only the application layer differs.
  • The body still begins at offset 28, and Flags2 is still the body length: 128 - 28 = 100 and 42 - 28 = 14, both matching.
  • The role byte is 0x86, where every *FA-SERVER frame uses 0x82. That confirms an observation made on 2026-07-29 which had been recorded without explanation.
  • Both sides use port 0x0605.
  • The service name is tagged the same way as everywhere else: FE 08 is string parameter 2 of length 8, holding *FA-USER. The parameter tagging rule carries over unchanged.

What is NOT decoded:

  • The first two words of the client frame, 0100 000A. The 000A is plausibly a length, since 10 bytes follow and they are exactly FE 08 plus the eight name characters - but the *FA-SERVER opener starts 1B 41 0012 instead, so the leading word is not the same field. One sample is not enough to say what either word is.
  • The whole server frame body, 0144 0060 0104 0066 0587 "USER" followed by 86 zero bytes. The words resemble system and port numbers (0066 is 102, 0064 is 100, 0587 and 0605 look like ports) but nothing here tests that reading.
  • Whether these two frames are the complete close exchange, or whether more would appear with a file that had actually been written to. The file closed here had never been written.

Getting further needs several more close recordings to diff against each other, in the way the CREATE-FILE name-length experiment was done. That has not been attempted.


6l. The whole operation table, from the server's own binary (2026-07-30)

Every operation code is now named. Not inferred from behaviour - read out of *FA-SERVER itself.

The server program COS-FA-SERV-E04:PROG is loaded in Ghidra as E:\Dev\Ronny\NDInsight\Installation\Communication\COSMOS Basic\x\cos-fa-serv-e04.prog, using an ND-100 :PROG loader. At BANK2::8731 it holds a word-aligned packed list of thirteen command names, immediately followed by a table of handler addresses:

index  0     1     2     3     4     5     6     7     8     9    10    11    12    13   14
addr 1fb0  1fb0  1ead  1ecc  1edd  1eee  1eff  1f10  1f21  1f32  1f43  1f6c  1f7d  1f8e 1f9f

Reading the names in order against that table gives:

Opcode Name How we know
0001 File-entry-disconnect handler slot points at the table's padding address, so not dispatched here
0002 Reserve-file-entry opens every recorded conversation
0003 Release-file-entry closes every recorded conversation
0004 Change-file-entry-id never recorded
0005 Open-file matches the recording
0006 Close-file resolves the exchange that had no name
0007 Set-block-size never recorded
0008 Read-file never recorded
0009 Write-file never recorded
000A Create-file matches the recording
000B Delete-file matches the recording
000C SIII-special resolves the "mystery 000C"
000D Device-function never recorded

Three independent matches against opcodes established earlier from recordings - Open-file, Create-file and Delete-file - are why the numbering is treated as VERIFIED rather than assumed.

What this corrects

0002 and 0003 are not "open spec" and "close spec". Those were names for what they looked like. They reserve and release a FILE ENTRY.

Operation 0006 IS the file close. Section 6k concluded it could not be, because it only appeared at session teardown and never when the operator typed CLOSE-FILE. The wire observation was right; the conclusion was wrong. Teardown issues a real close. The operator's CLOSE-FILE going to *FA-USER instead remains true and remains unexplained.

Selector 1 was never an inconsistent constant. 0x0078 on a LIST-FILES request and 0x003B at teardown looked like a broken rule. Both are SIII-special requests, and selector 1 carries its SUB-FUNCTION. Two sub-functions, not two values of one field. Which is which is UNKNOWN.

What it unblocks

Read-file and Write-file are ordinary operations 0008 and 0009. The terminal's RFILE and WFILE commands take a memory-address argument, which is why driving them by hand on a live machine was avoided - but the protocol operations carry no such thing, so a client can issue them safely. The handlers are at BANK2::1f21 and BANK2::1f32, so their request shapes can be read from the code rather than captured.

Method note

The strings alone were nearly enough - the names are in order and four of them line up with known opcodes. The handler table is what makes it evidence rather than a good guess, because it shows the opcode is used as an index.