pi-openai-toolkit 0.7.1 → 0.7.2

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.
package/README.md CHANGED
@@ -163,7 +163,7 @@ Content-Type: application/json
163
163
  - **Validation Criteria**: Must return a valid SSE stream with `response.completed` (status `completed`) containing exactly one output item of `type: "compaction"` with non-empty `encrypted_content`.
164
164
  - **Remote Model Override**: `compaction.remoteCompactModel` can assign a same-gateway Responses model as the checkpoint producer while the active model remains the consumer. Its registered model metadata, authentication, headers, and effective base URL are used only for the synthetic compact request. Existing and recursive checkpoints remain replayable by the active model.
165
165
  - **Zero Loss Replay**: The opaque encrypted item is stored in `CompactionEntry.details.compactedWindow`. On subsequent requests, it is transparently prepended after fresh preamble instructions alongside live turns, eliminating lossy text re-summaries.
166
- - **Fail-Closed Replay Guard**: Replay verifies only the compaction summary sentinel plus the kept pre-compaction window derived from session entries; leading/trailing prompts and the post-compaction live tail are preserved verbatim, so transient messages injected by other extensions no longer block replay. If the kept region cannot be verified (e.g. `expected-pi-replay-mismatch` or `compaction-summary-not-found`), the toolkit aborts the provider request, shows an error notification, and always writes a content-free `*-replay-failure.json` artifact even when `logProviderPayloads` is disabled. The sentinel-only payload is never sent to the model.
166
+ - **Fail-Closed Replay Guard**: Replay anchors on the Pi-authored compaction summary item by content and replaces it with the opaque compacted window; the post-compaction context is preserved verbatim, so transient messages injected by other extensions no longer block replay. If the summary anchor cannot be found, the toolkit aborts the provider request, shows an error notification, and always writes a content-free `*-replay-failure.json` artifact even when `logProviderPayloads` is disabled. The sentinel-only payload is never sent to the model.
167
167
  - **Graceful Multi-Tier Fallback**:
168
168
  1. Compatible Responses endpoints → Remote v2 Opaque Compaction.
169
169
  2. Gateway failure or non-Responses model → Configured `compaction.model` fallback.
package/README_zh.md CHANGED
@@ -163,7 +163,7 @@ Content-Type: application/json
163
163
  - **校验标准**:接收 SSE 流并验证 `response.completed` 事件(状态为 `completed`),且返回且仅返回一个包含非空 `encrypted_content` 密文的 `type: "compaction"` 数据块。
164
164
  - **远端压缩模型覆盖**:`compaction.remoteCompactModel` 可以让同一网关上的另一个 Responses 模型负责生成 checkpoint,而当前活动模型仍是 checkpoint 的消费者。synthetic 压缩请求会使用覆盖模型在注册表中的模型元数据、认证、请求头和实际 Base URL;旧 checkpoint、递归压缩 checkpoint 都仍由当前活动模型重放。
165
165
  - **无损重放机制**:密文数据完整存入 `CompactionEntry.details.compactedWindow`。在后续对话中,插件会自动将密文插入在最新的系统指令之后与最新几轮消息之前,实现完全服务端无损上下文还原。
166
- - **重放失败保护(Fail-Closed)**:重放只校验 compaction summary 哨兵项以及由 session entries 推导出的压缩前保留窗口;前置/后置 prompt 与压缩后的实时尾部消息一律原样保留,因此其他扩展注入的临时消息不再阻断重放。若保留窗口无法与 session entries 对齐(如 `expected-pi-replay-mismatch`、`compaction-summary-not-found`),插件会中止 provider 请求、弹出错误通知,并且即使 `logProviderPayloads` 关闭也会写入一份不含内容的安全失败记录 `*-replay-failure.json`。只含哨兵占位符的请求体永远不会发送给模型。
166
+ - **重放失败保护(Fail-Closed)**:重放以 Pi 生成的 compaction summary 哨兵为内容锚点,将其替换为密文压缩窗口;压缩后的其余上下文一律原样保留,因此其他扩展注入的临时消息不再阻断重放。若找不到哨兵锚点,插件会中止 provider 请求、弹出错误通知,并且即使 `logProviderPayloads` 关闭也会写入一份不含内容的安全失败记录 `*-replay-failure.json`。只含哨兵占位符的请求体永远不会发送给模型。
167
167
  - **三级优雅降级**:
168
168
  1. 支持的 Responses 端点 → 触发 Remote v2 密文无损压缩;
169
169
  2. 网关不支持或报错 → 降级调用用户指定的 `compaction.model`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-toolkit",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "OpenAI-focused Pi toolkit providing remote Responses compaction and native Web Search.",
@@ -548,12 +548,10 @@ async function handleBeforeProviderRequest(event: BeforeProviderRequestEvent, ct
548
548
  firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
549
549
  originalInputItems: payload.input.length,
550
550
  rewrittenInputItems: rewrite.rewrittenPayload.input.length,
551
- freshPreambleItems: rewrite.segments.freshPreamble.length,
552
- trailingPreambleItems: rewrite.segments.trailingPreamble.length,
551
+ leadingItems: rewrite.segments.leading.length,
553
552
  compactionSummaryItems: rewrite.segments.compactionSummary.length,
554
- preCompactionKeptItems: rewrite.segments.preCompactionKeptWindow.input.length,
555
553
  compactedItems: rewrite.segments.compactedWindow.length,
556
- postCompactionTailItems: rewrite.segments.postCompactionTail.input.length,
554
+ postItems: rewrite.segments.post.length,
557
555
  payload: rewrite.rewrittenPayload,
558
556
  originalPayload: payload,
559
557
  },
@@ -9,31 +9,16 @@ import type {
9
9
  import type { ResponsesCompatibleRequestPayload } from "./runtime";
10
10
  import { rewritePayloadWithDeferredToolCarryover } from "./deferred-tool-carryover";
11
11
  import type { NativeCompactionEntry } from "./types";
12
- import {
13
- compareResponsesInputParity,
14
- serializeMessagesToResponsesInput,
15
- type ResponsesInputContentItem,
16
- type ResponsesInputItem,
17
- type ResponsesInputMessageItem,
18
- } from "./serializer";
19
-
20
- export type SerializedReplaySlice = {
21
- entries: SessionEntry[];
22
- messages: AgentMessage[];
23
- input: ResponsesInputItem[];
24
- };
12
+ import { serializeMessagesToResponsesInput, type ResponsesInputContentItem, type ResponsesInputItem, type ResponsesInputMessageItem } from "./serializer";
25
13
 
26
14
  export type NativeReplaySegments = {
27
15
  boundaryIndex: number;
28
16
  firstKeptEntryIndex: number;
29
17
  instructions?: string;
30
- freshPreamble: ResponsesInputMessageItem[];
31
- trailingPreamble: ResponsesInputMessageItem[];
18
+ leading: ResponsesInputItem[];
32
19
  compactionSummary: ResponsesInputItem[];
33
- preCompactionKeptWindow: SerializedReplaySlice;
34
20
  compactedWindow: unknown[];
35
- postCompactionTail: SerializedReplaySlice;
36
- originalPiReplayInput: ResponsesInputItem[];
21
+ post: ResponsesInputItem[];
37
22
  replayInput: unknown[];
38
23
  };
39
24
 
@@ -48,7 +33,6 @@ export type NativeReplayPayloadRewriteFailureReason =
48
33
  | "first-kept-entry-not-found"
49
34
  | "invalid-compacted-window"
50
35
  | "unexpected-compaction-after-boundary"
51
- | "expected-pi-replay-mismatch"
52
36
  | "compaction-summary-not-found";
53
37
 
54
38
  export type NativeReplayPayloadRewriteFailure = {
@@ -89,19 +73,6 @@ function isResponsesInputMessageRole(value: unknown): value is ResponsesInputMes
89
73
  return value === "user" || value === "developer" || value === "system";
90
74
  }
91
75
 
92
- function isPreambleRole(value: ResponsesInputMessageItem["role"]): value is "developer" | "system" {
93
- return value === "developer" || value === "system";
94
- }
95
-
96
- function isResponsesInputMessageItem(value: unknown): value is ResponsesInputMessageItem {
97
- if (!isRecord(value) || !isResponsesInputMessageRole(value.role)) {
98
- return false;
99
- }
100
-
101
- const { content } = value;
102
- return typeof content === "string" || (Array.isArray(content) && content.every(isResponsesInputContentItem));
103
- }
104
-
105
76
  function cloneResponsesInputContentItem(item: ResponsesInputContentItem): ResponsesInputContentItem {
106
77
  return item.type === "input_text"
107
78
  ? {
@@ -294,20 +265,6 @@ function findCompactionSummaryIndex(input: readonly unknown[], summaryMarker: st
294
265
  });
295
266
  }
296
267
 
297
- /** Count consecutive provider-authored developer/system items at the end of the input. */
298
- function countTrailingPreambleItems(items: readonly unknown[]): number {
299
- let count = 0;
300
- for (let index = items.length - 1; index >= 0; index--) {
301
- const item = items[index];
302
- if (isResponsesInputMessageItem(item) && isPreambleRole(item.role)) {
303
- count += 1;
304
- continue;
305
- }
306
- break;
307
- }
308
- return count;
309
- }
310
-
311
268
  function collectReplayMessages(entries: readonly SessionEntry[]): AgentMessage[] {
312
269
  const messages: AgentMessage[] = [];
313
270
 
@@ -321,27 +278,6 @@ function collectReplayMessages(entries: readonly SessionEntry[]): AgentMessage[]
321
278
  return messages;
322
279
  }
323
280
 
324
- function createCompactionSummaryAgentMessage(entry: NativeCompactionEntry): AgentMessage {
325
- return {
326
- role: "compactionSummary",
327
- summary: entry.summary,
328
- tokensBefore: entry.tokensBefore,
329
- timestamp: new Date(entry.timestamp).getTime(),
330
- } as AgentMessage;
331
- }
332
-
333
- function createReplaySlice(
334
- entries: readonly SessionEntry[],
335
- messages: readonly AgentMessage[],
336
- input: readonly ResponsesInputItem[],
337
- ): SerializedReplaySlice {
338
- return {
339
- entries: [...entries],
340
- messages: [...messages],
341
- input: [...input],
342
- };
343
- }
344
-
345
281
  function findEntryIndexByIdBeforeBoundary(
346
282
  entries: readonly SessionEntry[],
347
283
  entryId: string,
@@ -436,14 +372,13 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
436
372
 
437
373
  const preCompactionEntries = args.branchEntries.slice(firstKeptEntryIndex, boundaryIndex);
438
374
  const postCompactionEntries = args.branchEntries.slice(boundaryIndex + 1);
439
- const preCompactionKeptMessages = collectReplayMessages(preCompactionEntries);
440
- const postCompactionTailMessages = collectReplayMessages(postCompactionEntries);
441
- const compactionSummaryMessage = createCompactionSummaryAgentMessage(args.compactionEntry);
442
- const keptWindowSerialized = serializeMessagesToResponsesInput(args.model, preCompactionKeptMessages);
443
-
444
- // Pi-authored compaction summary item. Everything outside the replaced region
445
- // is preserved verbatim, so this content anchor is the only location probe
446
- // needed; the marker text is unique to this compaction entry.
375
+
376
+ // Pi-authored compaction summary item. Pi 0.84.3 renders the context after a
377
+ // compaction as [leading prompts][summary item][all post-compaction messages]:
378
+ // pre-compaction messages never appear in the payload (entry.retainedTail is
379
+ // never persisted by appendCompaction), so the summary item itself is the only
380
+ // anchor and the only thing that gets replaced. Everything else is preserved
381
+ // verbatim, so transient messages injected by other extensions stay intact.
447
382
  const summaryIndex = findCompactionSummaryIndex(args.payload.input, args.compactionEntry.summary);
448
383
  if (summaryIndex < 0) {
449
384
  return {
@@ -452,66 +387,19 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
452
387
  };
453
388
  }
454
389
 
455
- const keptStart = summaryIndex + 1;
456
- const keptEnd = keptStart + keptWindowSerialized.length;
457
- const actualKeptWindow = cloneResponsesInputSlice(args.payload.input.slice(keptStart, keptEnd));
458
- if (!actualKeptWindow) {
459
- return {
460
- ok: false,
461
- reason: "expected-pi-replay-mismatch",
462
- };
463
- }
464
-
465
- // The only hard verification: the region immediately after the sentinel must be
466
- // exactly the kept pre-compaction window derived from session entries. Content
467
- // after this region is preserved as-is, so transient messages injected by other
468
- // extensions (which have no session entries) no longer break replay.
469
- if (!areEquivalentValues(actualKeptWindow, keptWindowSerialized)) {
470
- const parity = compareResponsesInputParity(
471
- args.payload.input.slice(keptStart, keptEnd),
472
- keptWindowSerialized,
473
- );
474
- return {
475
- ok: false,
476
- reason: "expected-pi-replay-mismatch",
477
- parity: {
478
- actual: parity.actual,
479
- expected: parity.expected,
480
- mismatches: parity.mismatches,
481
- },
482
- };
483
- }
484
-
485
- const actualCompactionSummary = cloneResponsesInputSlice([args.payload.input[summaryIndex]]);
486
- const actualPostCompactionTail = cloneResponsesInputSlice(args.payload.input.slice(keptEnd));
487
- if (!actualCompactionSummary || !actualPostCompactionTail) {
488
- return {
489
- ok: false,
490
- reason: "expected-pi-replay-mismatch",
491
- };
492
- }
493
-
494
- // Keep provider-authored trailing developer/system prompts in place.
495
- const trailingCount = countTrailingPreambleItems(actualPostCompactionTail);
496
- const trailingPreamble = actualPostCompactionTail.slice(actualPostCompactionTail.length - trailingCount);
497
- const liveTail = actualPostCompactionTail.slice(0, actualPostCompactionTail.length - trailingCount);
498
-
499
390
  const leadingInput = cloneResponsesInputSlice(args.payload.input.slice(0, summaryIndex));
500
- if (!leadingInput) {
391
+ const actualCompactionSummary = cloneResponsesInputSlice([args.payload.input[summaryIndex]]);
392
+ const actualPostInput = cloneResponsesInputSlice(args.payload.input.slice(summaryIndex + 1));
393
+ if (!leadingInput || !actualCompactionSummary || !actualPostInput) {
501
394
  return {
502
395
  ok: false,
503
- reason: "expected-pi-replay-mismatch",
396
+ reason: "compaction-summary-not-found",
504
397
  };
505
398
  }
506
399
 
507
400
  const baseRewrittenPayload: ResponsesCompatibleRequestPayload = {
508
401
  ...args.payload,
509
- input: [
510
- ...leadingInput,
511
- ...compactedWindow,
512
- ...liveTail,
513
- ...trailingPreamble,
514
- ],
402
+ input: [...leadingInput, ...compactedWindow, ...actualPostInput],
515
403
  };
516
404
  const carryoverRewrite = rewritePayloadWithDeferredToolCarryover({
517
405
  payload: baseRewrittenPayload,
@@ -523,33 +411,16 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
523
411
  | undefined,
524
412
  });
525
413
 
526
- const sentinelSerialized = serializeMessagesToResponsesInput(args.model, [compactionSummaryMessage]);
527
- const serializedTailInput = serializeMessagesToResponsesInput(args.model, postCompactionTailMessages);
528
- const originalPiReplayInput: ResponsesInputItem[] = [
529
- ...leadingInput,
530
- ...sentinelSerialized,
531
- ...keptWindowSerialized,
532
- ...serializedTailInput,
533
- ...trailingPreamble,
534
- ];
535
-
536
414
  return {
537
415
  ok: true,
538
416
  segments: {
539
417
  boundaryIndex,
540
418
  firstKeptEntryIndex,
541
419
  instructions: typeof args.payload.instructions === "string" ? args.payload.instructions : undefined,
542
- freshPreamble: leadingInput as ResponsesInputMessageItem[],
543
- trailingPreamble: trailingPreamble as ResponsesInputMessageItem[],
420
+ leading: leadingInput,
544
421
  compactionSummary: actualCompactionSummary,
545
- preCompactionKeptWindow: createReplaySlice(
546
- preCompactionEntries,
547
- preCompactionKeptMessages,
548
- actualKeptWindow,
549
- ),
550
422
  compactedWindow,
551
- postCompactionTail: createReplaySlice(postCompactionEntries, postCompactionTailMessages, liveTail),
552
- originalPiReplayInput,
423
+ post: actualPostInput,
553
424
  replayInput: carryoverRewrite.payload.input,
554
425
  },
555
426
  rewrittenPayload: carryoverRewrite.payload,