@akagilnc/pi-workflow-roles 0.1.4295 → 0.1.4321

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -112,8 +112,8 @@ function isCodexOwnerResponseItemUser(message: Record<string, unknown>): boolean
112
112
 
113
113
  /**
114
114
  * 入队事件适配:人类这次输入经队列的来源。
115
- * 配对的出队事件不另取,也不做正文比对去重——被吸收的插话正是靠入队事件入录
116
- * (它没有独立的消息记录,#901 用户故事 11)。
115
+ * 被吸收的插话靠入队事件入录(无独立消息记录,#901 用户故事 11)。
116
+ * 机器入队由整卷适配层排除(#918);本函数不读正文、不自判机器。
117
117
  */
118
118
  function fromQueueEvent(row: Record<string, unknown>): DialogueEvent[] {
119
119
  if (row.type !== "queue-operation" || row.operation !== "enqueue") return [];
@@ -123,6 +123,57 @@ function fromQueueEvent(row: Record<string, unknown>): DialogueEvent[] {
123
123
  return [{ speaker: "owner", text: content, ...(id === undefined ? {} : { id }) }];
124
124
  }
125
125
 
126
+ /**
127
+ * 票面 #918 准许的唯一固定起始形状:`<task-notification`。
128
+ * 其它标签不在御裁射程内——不得扩表,不得伪造 decision key / 「陛下拍定」。
129
+ * 开标签后可直接 `>`(无属性)、EOF 或空白再接属性;其它字符不成立。
130
+ *
131
+ * 机器 enqueue 的完整判据=结构化来源(origin.kind)/本固定形状/真实因果与位置;
132
+ * 用可变载荷正文(全等或子串)做 identity join 仍为锚定宪法所禁。
133
+ */
134
+ const TASK_NOTIFICATION_OPEN_TAG = "<task-notification";
135
+
136
+ function hasFixedOpenTagPrefix(content: string, tag: string): boolean {
137
+ if (!content.startsWith(tag)) return false;
138
+ const next = content.charAt(tag.length);
139
+ return (
140
+ next === "" ||
141
+ next === ">" ||
142
+ next === " " ||
143
+ next === "\t" ||
144
+ next === "\n" ||
145
+ next === "\r"
146
+ );
147
+ }
148
+
149
+ /** enqueue.content 是否以票面准许的 `<task-notification` 固定开标签起头。 */
150
+ function isFixedNonOwnerEnqueueShape(content: string): boolean {
151
+ return hasFixedOpenTagPrefix(content, TASK_NOTIFICATION_OPEN_TAG);
152
+ }
153
+
154
+ /**
155
+ * 物化 user 行上的结构化来源 kind(CC top-level `origin.kind`)。不读正文。
156
+ * 活体取值含 human / task-notification / peer;缺字段=旧形无 provenance。
157
+ */
158
+ function materializationOriginKind(
159
+ row: Record<string, unknown>,
160
+ ): string | undefined {
161
+ if (!isRecord(row.origin)) return undefined;
162
+ const kind = row.origin.kind;
163
+ return typeof kind === "string" && kind !== "" ? kind : undefined;
164
+ }
165
+
166
+ /**
167
+ * origin.kind 取值是否为非陛下来源(#918 第二类)。
168
+ * - 缺 origin 或 kind=human → 真人路(经队列的真人输入必须保留)
169
+ * - task-notification / peer / 其他具名非 human → 机器或跨会话投递,不得署 owner
170
+ * 判别落在 kind 取值上,不落在字段是否存在。
171
+ */
172
+ function isNonOwnerOriginKind(kind: string | undefined): boolean {
173
+ if (kind === undefined) return false;
174
+ return kind !== "human";
175
+ }
176
+
126
177
  /**
127
178
  * 消息事件适配:助手回话,以及未经入队事件记录的人类输入。
128
179
  * 思考块、工具调用块、工具结果块按块排除;同消息的说话人 text 保留
@@ -148,78 +199,30 @@ function fromMessageEvent(row: Record<string, unknown>): DialogueEvent[] {
148
199
  return [{ speaker, text, ...(id === undefined ? {} : { id }) }];
149
200
  }
150
201
 
151
- /** 该行是否为可产出 owner 对话正文的消息(工具结果块不算;旁路 text 算)。 */
152
- function isOwnerDialogueMessage(row: Record<string, unknown>): boolean {
153
- // CC/Pi queue pairing only — Codex owner path is separate.
154
- if (row.type === "response_item" || row.type === "event_msg") return false;
155
- return fromMessageEvent(row).some((event) => event.speaker === "owner");
156
- }
157
-
158
- /** 该行是否为 runner 回话事件(用于解除未物化的 dequeue 配对)。 */
159
- function isRunnerMessage(row: Record<string, unknown>): boolean {
160
- const message = messageBody(row);
161
- return message !== undefined && message.role === "assistant";
162
- }
163
-
164
- /**
165
- * 逐次来源配对:仅当对应 enqueue 已由 fromQueueEvent 实际形成 retained owner
166
- * 对话时,才把随后的物化 user 当副本跳过。FIFO 对齐 enqueue→dequeue/remove。
167
- * 无正文 enqueue、旧宿主或其他不可投影形状:dequeue 不产生 skip,后续真人
168
- * user 原样保留。未物化的 retained dequeue(被吸收插话)遇 runner 解除。
169
- * 不用「卷内曾出现 enqueue」整卷布尔,也不按正文过滤。
170
- */
171
- function ownerMessagesMaterializingQueue(
172
- rows: readonly (Record<string, unknown> | undefined)[],
173
- ): ReadonlySet<number> {
174
- const skip = new Set<number>();
175
- /** 各 enqueue 是否已留下 owner 对话,按入队顺序等 dequeue/remove 消费。 */
176
- const retainedByEnqueue: boolean[] = [];
177
- let pendingSkips = 0;
178
- for (let index = 0; index < rows.length; index += 1) {
179
- const row = rows[index];
180
- if (row === undefined) continue;
181
- if (row.type === "queue-operation") {
182
- if (row.operation === "enqueue") {
183
- retainedByEnqueue.push(fromQueueEvent(row).length > 0);
184
- continue;
185
- }
186
- if (row.operation === "dequeue" || row.operation === "remove") {
187
- const retained = retainedByEnqueue.shift();
188
- // 只在 enqueue 真留下对话且本事件是 dequeue 物化时才跳过后续 user。
189
- // remove 只消费队列槽,不制造 skip(无物化消息)。
190
- if (row.operation === "dequeue" && retained === true) {
191
- pendingSkips += 1;
192
- }
193
- continue;
194
- }
195
- }
196
- if (isRunnerMessage(row)) {
197
- pendingSkips = 0;
198
- continue;
199
- }
200
- if (pendingSkips > 0 && isOwnerDialogueMessage(row)) {
201
- skip.add(index);
202
- pendingSkips -= 1;
203
- }
204
- }
205
- return skip;
206
- }
207
-
208
202
  /**
209
203
  * 整卷适配:返回与入参行等长的对话事实数组(该行不是对话则为空数组)。
210
- * 需要整卷视野:enqueue/dequeue 配对跨行。
204
+ *
205
+ * 物化 user 只消费自身的 typed `origin.kind`:human 保留,具名非 human 排除。
206
+ * enqueue 没有与物化 user 的 typed 关联,故不得以位置把某个 user 来源反扣给它;
207
+ * 仅票面准许的 `<task-notification` 固定形状可直接排除,其余维持既有投影。
211
208
  */
212
209
  export function adaptSessionDialogue(
213
210
  rows: readonly (Record<string, unknown> | undefined)[],
214
211
  ): DialogueEvent[][] {
215
- const skipOwner = ownerMessagesMaterializingQueue(rows);
216
- return rows.map((row, index) => {
212
+ return rows.map((row) => {
217
213
  if (row === undefined) return [];
214
+ const originKind = materializationOriginKind(row);
215
+ if (isNonOwnerOriginKind(originKind)) return [];
216
+ if (
217
+ row.type === "queue-operation" &&
218
+ row.operation === "enqueue" &&
219
+ typeof row.content === "string" &&
220
+ isFixedNonOwnerEnqueueShape(row.content)
221
+ ) {
222
+ return [];
223
+ }
218
224
  const queued = fromQueueEvent(row);
219
225
  if (queued.length > 0) return queued;
220
- // Codex event_msg.user_message 无 content_item_kinds 等 provenance,旧 exec
221
- // 卷中与 worker entrypoint 注入同形——无法证明为 owner 则不取(不猜、不咬正文)。
222
- if (skipOwner.has(index)) return [];
223
226
  return fromMessageEvent(row);
224
227
  });
225
228
  }
@@ -4,8 +4,7 @@
4
4
  *
5
5
  * Write paths:
6
6
  * - sitianReport → appender (SitianRecord rows, append + identity claim)
7
- * - rewriteSitianVolume whole-file reproject (ticket-provenance bare lines, #901)
8
- * Read paths: readSitianRecords (canonical rows) / readSitianVolumeText (raw body).
7
+ * Read paths: readSitianRecords (canonical rows).
9
8
  */
10
9
  import { appendSitianRecord } from "./sitian-appender.ts";
11
10
  import type { RecordPointer, SitianRecordInput } from "./sitian-contracts.ts";
@@ -13,7 +12,6 @@ import type { RecordPointer, SitianRecordInput } from "./sitian-contracts.ts";
13
12
  export * from "./sitian-contracts.ts";
14
13
  export * from "./sitian-appender.ts";
15
14
  export * from "./sitian-reader.ts";
16
- export * from "./sitian-volume.ts";
17
15
 
18
16
  /**
19
17
  * Sole append API for Sitian canonical records.
@@ -1,9 +1,10 @@
1
1
  /**
2
- * 起居录(ticket-provenance)typed 形状 —— ADR 0075「2026-09-14 修订」/ #901。
2
+ * 起居录(ticket-provenance)typed 形状 —— ADR 0075「2026-09-14 修订」/ #901 / #918
3
3
  *
4
- * 一册=一个文件:第一行册子头,其后每行一条对话。
5
- * 每轮按册子头当前各区间**重投影**这份唯一文件:册子头与各条定位可更新,
6
- * 正文原样不改(`single-volume` / `one-volume-per-issue`)。
4
+ * 一册=一个追加式 records.jsonl;逻辑册子头与对话由读取时折叠不可变投影提交得到。
5
+ * 既有「首行册子头+裸对话行」snapshot 向后可读,后续提交不回写旧行。
6
+ * sessions prior ∪ 各轮累计并集(遗漏不删除),已结转正文不因历史源重读而改。
7
+ * 交卷 `sessions` 输入语义仍是「本轮对话边界」;累计并集是机械合并,不是角色交什么。
7
8
  */
8
9
 
9
10
  /** Sitian kind for per-ticket court diary volumes. */
@@ -27,13 +28,16 @@ export type TicketProvenanceRange = {
27
28
  readonly to: TicketProvenanceBound;
28
29
  };
29
30
 
30
- /** 册子头记的一卷:会话卷路径 + 本轮该卷的各区间。 */
31
+ /**
32
+ * 一卷:会话卷路径 + 区间声明。
33
+ * 交卷时=本轮边界;写入册子头后=累计并集(#918),二者同型。
34
+ */
31
35
  export type TicketProvenanceSession = {
32
36
  readonly path: string;
33
37
  readonly ranges: readonly TicketProvenanceRange[];
34
38
  };
35
39
 
36
- /** 册子头(文件第一行)。reopen 与跨宿主为新增区间,不新建册。 */
40
+ /** 读取时折叠出的逻辑册子头。sessions 为累计区间;reopen 与跨宿主并入,不新建册。 */
37
41
  export type TicketProvenanceHeader = {
38
42
  readonly repo: string;
39
43
  readonly ticket: number;
@@ -43,7 +47,7 @@ export type TicketProvenanceHeader = {
43
47
  };
44
48
 
45
49
  /**
46
- * 一条对话(册子头之后每行一条)。
50
+ * 一条对话(存于不可变投影提交;旧 snapshot 中也可为裸行)。
47
51
  * `id` 与 `line` 按源事实存在则记、不存在不编;两者皆无的少数条目
48
52
  * 靠录中前后有定位的条目锚定。
49
53
  */